package com.leyou.search.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.gitee.yongzhuzl.commonutil.util.empty.EmptyUtil;
import com.gitee.yongzhuzl.commonutil.util.equals.EqualsUtil;
import com.gitee.yongzhuzl.commonutil.util.json.JsonTool;
import com.gitee.yongzhuzl.commonutil.util.json.JsonUtil;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.utils.NumberUtils;
import com.leyou.common.vo.PageResult;
import com.leyou.item.api.BrandApi;
import com.leyou.item.api.CategoryApi;
import com.leyou.item.api.GoodsApi;
import com.leyou.item.api.SpecificationApi;
import com.leyou.item.pojo.*;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.pojo.SearchResult;
import com.leyou.search.repository.GoodsRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;

/**
 * description: elasticsearch索引库相关操作
 * @author 朱正磊
 * @date 2021-04-08 15:59:17
 */
@Slf4j
@Service
public class SearchService {

    @Resource
    private GoodsApi goodsApi;

    @Resource
    private CategoryApi categoryApi;

    @Resource
    private BrandApi brandApi;

    @Resource
    private SpecificationApi specificationApi;

    @Resource
    private GoodsRepository goodsRepository;

    @Resource
    private ElasticsearchTemplate elasticsearchTemplate;

    /**
     * description: 利用数据库Spu对象构建索引库Goods对象
     * @author 朱正磊
     * @date 2021-04-08 16:03:12
     * @param spu 商品对象
     * @return 返回Goods对象
     */
    public Goods buildGoods(Spu spu) {
        // 获取spuId
        Long spuId = spu.getId();
        // 查询分类
        List<Long> cidList = Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3());
        List<Category> categoryList = categoryApi.queryByIds(cidList);
        if (EmptyUtil.isEmpty(categoryList)) {
            throw new LyException(ExceptionEnum.CATEGORY_NOT_FOUND);
        }
        List<String> names = categoryList.stream().map(Category::getName).collect(Collectors.toList());
        String cName = StringUtils.join(names, " ");
        // 查询品牌
        Brand brand = brandApi.queryById(spu.getBrandId());
        if (brand == null) {
            throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
        }
        String brandName = brand.getName();
        // 搜索字段
        String all = spu.getTitle() + cName + brandName;

        // 查询sku
        List<Sku> skuList = goodsApi.querySkuListBySpuId(spuId);
        if (EmptyUtil.isEmpty(skuList)) {
            throw new LyException(ExceptionEnum.GOODS_SKU_NOT_FOUND);
        }
        // 为了提高性能，避免频繁for循环遍历操作，我们这里就不使用流遍历了，在下面for循环的时候处理价格即可
        // Set<Long> priceSet = skuList.stream().map(Sku::getPrice).collect(Collectors.toSet())
        Set<Long> priceSet = new LinkedHashSet<>();
        // 对sku进行处理
        List<Map<String, Object>> skus = new ArrayList<>();
        for (Sku sku : skuList) {
            Map<String, Object> map = new HashMap<>(16);
            map.put("id", sku.getId());
            map.put("title", sku.getTitle());
            map.put("price", sku.getPrice());
            map.put("image", StringUtils.substringBefore(sku.getImages(), ","));
            skus.add(map);
            // 价格处理
            priceSet.add(sku.getPrice());
        }

        // 根据三级分类id和为搜索规格参数查询规格参数集合
        List<SpecParam> params = specificationApi.querySpecParams(null, spu.getCid3(), true);
        if (EmptyUtil.isEmpty(params)) {
            throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND);
        }
        SpuDetail spuDetail = goodsApi.queryDetailById(spuId);
        if (EmptyUtil.isEmpty(spuDetail)) {
            throw new LyException(ExceptionEnum.GOODS_DETAIL_NOT_FOUND);
        }

        // 获取通用规格参数
        // Map<String, Object> genericSpec = JsonTool.jsonToPojo(spuDetail.getGenericSpec(), Map.class); //
        Map<String, Object> genericSpec = JsonUtil.toMap(spuDetail.getGenericSpec(), String.class, Object.class);

        // 获取特有规格参数
        // Map<String, List<Object>> specialSpec = JsonTool.jsonToPojo(spuDetail.getSpecialSpec(), Map.class); //
        Map<String, List<Object>> specialSpec = JsonUtil.toCustom(spuDetail.getSpecialSpec(),
                new TypeReference<Map<String, List<Object>>>() {});

        // 规格参数，其中key是规格参数名称，value是规格参数值
        Map<String, Object> specs = new HashMap<>(16);
        for (SpecParam param : params) {
            // 规格参数名称
            String name = param.getName();
            // 规格参数值
            Object value;
            // 判断是否是通用规格参数
            if (param.getGeneric()) {
                value = genericSpec.get(param.getId().toString());
                // 判断是否是数值类型
                if (param.getNumeric()) {
                    // 处理成数值区间
                    if (EmptyUtil.isNotEmpty(value)) {
                        value = this.chooseSegment(value.toString(), param);
                    } else {
                        log.error("获取数值类型的通用规格参数失败：{}", genericSpec);
                    }
                }
            } else {
                value = specialSpec.get(param.getId().toString());
            }
            // 存入规格参数
            specs.put(name, value);
        }

        // 构建goods对象
        Goods goods = new Goods();
        goods.setBrandId(spu.getBrandId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setId(spuId);
        goods.setSubTitle(spu.getSubTitle());
        goods.setCreateTime(spu.getCreateTime());

        // 搜索字段，包括标题、分类、品牌、规格参数等等
        goods.setAll(all);

        // 所有sku的价格集合
        goods.setPrice(priceSet);

        // 所有sku的集合的json格式
        goods.setSkus(JsonTool.objectToJson(skus));

        // 所有的可搜索的规格参数
        goods.setSpecs(specs);

        return goods;
    }

    /**
     * description: 根据数值和规格参数对象获取数值所在区间
     * @author 朱正磊
     * @date 2021-04-08 18:35:48
     * @param value 数值
     * @param p 规格参数对象
     * @return 返回数值所在区间
     */
    private String chooseSegment(String value, SpecParam p) {
        double val = NumberUtils.toDouble(value);
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = NumberUtils.toDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if(segs.length == 2){
                end = NumberUtils.toDouble(segs[1]);
            }
            // 判断是否在范围内
            if(val >= begin && val < end){
                if(segs.length == 1){
                    result = segs[0] + p.getUnit() + "以上";
                }else if(begin == 0){
                    result = segs[1] + p.getUnit() + "以下";
                }else{
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    /**
     * description: 商品搜索功能
     * @author 朱正磊
     * @date 2021-04-09 00:55:25
     * @param searchRequest 搜索请求参数
     * @return 返回商品分页数据
     */
    public PageResult<Goods> search(SearchRequest searchRequest) {
        // 当前页
        int page = searchRequest.getPage();
        // 每页大小
        int size = searchRequest.getSize();

        // 创建Spring提供的原生查询构建器（可以把原生查询构建在一起，进而简化代码开发）
        NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();

        // 分页，页码从0开始，即页码为0表示第1页
        searchQueryBuilder.withPageable(PageRequest.of(page - 1, size));

        // 过滤
        // 构造基本查询条件以及过滤条件
        QueryBuilder basicQueryBuilder = this.buildBasicQuery(searchRequest);
        searchQueryBuilder.withQuery(basicQueryBuilder);

        // 查询结果过滤
        SourceFilter sourceFilter = new FetchSourceFilter(new String[]{"id", "subTitle", "skus"}, null);
        searchQueryBuilder.withSourceFilter(sourceFilter);

        // 聚合分类和品牌
        // 聚合分类
        String categoryAggName = "category_agg";
        searchQueryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
        // 聚合品牌
        String brandAggName = "brand_agg";
        searchQueryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));

        // 利用原生构建器创建最终的查询对象
        SearchQuery searchQuery = searchQueryBuilder.build();
        // 执行查询
        // Page<Goods> result = goodsRepository.search(searchQuery); // 该方法只能满足复杂查询，不能实现聚合查询
        AggregatedPage<Goods> result = elasticsearchTemplate.queryForPage(searchQuery, Goods.class);

        // 解析分页结果
        // 总条数
        long total = result.getTotalElements();
        // 总页数
        int totalPage = result.getTotalPages();
        // 当前页结果
        List<Goods> goodsList = result.getContent();

        // 解析聚合结果
        Aggregations aggs = result.getAggregations();
        List<Category> categories = this.parseCategoryAgg(aggs.get(categoryAggName));
        List<Brand> brands = this.parseBrandAgg(aggs.get(brandAggName));

        // 商品规格参数聚合
        List<Map<String, Object>> specs = null;
        if (categories != null && categories.size() == 1) {
            // 商品分类存在并且数量为1才可以进行商品规格参数聚合
            // 我们需要在原来基本搜索的基础上进行规格参数聚合
            specs = this.buildSpecificationAgg(categories.get(0).getId(), basicQueryBuilder);
        }

        // 返回分页以及聚合结果
        return new SearchResult(total, totalPage, goodsList, categories, brands, specs);
    }

    /**
     * description: 构建查询条件和过滤条件，即构建布尔查询
     * @author 朱正磊
     * @date 2021-04-10 17:36:57
     * @param searchRequest 搜索请求参数封装对象
     * @return 返回QueryBuilder对象
     */
    private QueryBuilder buildBasicQuery(SearchRequest searchRequest) {
        // 创建布尔查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        // 查询条件
        boolQueryBuilder.must(QueryBuilders.matchQuery("all", searchRequest.getKey()));
        // 过滤条件
        Map<String, String> filter = searchRequest.getFilter();
        Set<Map.Entry<String, String>> entries = filter.entrySet();
        for (Map.Entry<String, String> entry : entries) {
            // 处理key
            String key = entry.getKey();
            if (EqualsUtil.isNotEquals(key, "cid3") && EqualsUtil.isNotEquals(key, "brandId")) {
                key = "specs." + key + ".keyword";
            }
            boolQueryBuilder.filter(QueryBuilders.termsQuery(key, entry.getValue()));
        }
        return boolQueryBuilder;
    }

    /**
     * description: 商品规格参数聚合
     * @author 朱正磊
     * @date 2021-04-09 20:11:13
     * @param cid 商品分类id
     * @param basicQueryBuilder 基本查询条件
     * @return 返回商品规格参数key以及待选项
     */
    private List<Map<String, Object>> buildSpecificationAgg(Long cid, QueryBuilder basicQueryBuilder) {
        List<Map<String, Object>> specs = new ArrayList<>();
        // 1、查询需要聚合的规格参数
        List<SpecParam> params = specificationApi.querySpecParams(null, cid, true);

        // 2、进行聚合
        NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();
        // 我们在聚合的时候需要带上查询条件
        searchQueryBuilder.withQuery(basicQueryBuilder);
        for (SpecParam param : params) {
            String name = param.getName();
            searchQueryBuilder.addAggregation(AggregationBuilders
                    .terms(name).field("specs." + name + ".keyword"));
        }

        // 3、获取聚合结果
        SearchQuery searchQuery = searchQueryBuilder.build();
        AggregatedPage<Goods> result = elasticsearchTemplate.queryForPage(searchQuery, Goods.class);

        // 4、解析聚合结果
        Aggregations aggs = result.getAggregations();
        for (SpecParam param : params) {
            String name = param.getName();
            StringTerms terms = aggs.get(name);
            List<String> options = terms.getBuckets().stream()
                    .map(StringTerms.Bucket::getKeyAsString)
                    .collect(Collectors.toList());
            // 准备spec
            Map<String, Object> spec = new HashMap<>(16);
            // 规格参数名称
            spec.put("k", name);
            // 规格参数待选项
            spec.put("options", options);
            specs.add(spec);
        }

        return specs;
    }

    /**
     * description: 解析品牌聚合
     * @author 朱正磊
     * @date 2021-04-09 16:51:05
     * @param terms 聚合对象
     * @return 返回品牌集合
     */
    private List<Brand> parseBrandAgg(LongTerms terms) {
        try {
            // 获取品牌id集合
            List<Long> ids = terms.getBuckets().stream()
                    .map(bucket -> bucket.getKeyAsNumber().longValue())
                    .collect(Collectors.toList());
            // 根据品牌id集合查询品牌集合
            return brandApi.queryByIds(ids);
        } catch (Exception e) {
            log.error("【搜索服务】查询品牌异常：", e);
            // 如果没有查询到品牌信息返回null给前端页面
            return null;
        }
    }

    /**
     * description: 解析分类聚合
     * @author 朱正磊
     * @date 2021-04-09 16:51:12
     * @param terms 聚合对象
     * @return 返回分类集合
     */
    private List<Category> parseCategoryAgg(LongTerms terms) {
        try {
            // 获取分类id集合
            List<Long> ids = terms.getBuckets().stream()
                    .map(bucket -> bucket.getKeyAsNumber().longValue())
                    .collect(Collectors.toList());
            // 根据分类id集合查询分类集合
            return categoryApi.queryByIds(ids);
        } catch (Exception e) {
            log.error("【搜索服务】查询商品分类异常：", e);
            // 如果没有查询到分类信息返回null给前端页面
            return null;
        }
    }

    /**
     * description: 对索引库进行商品的新增或者修改
     * @author 朱正磊
     * @date 2021-04-14 01:10:01
     * @param spuId spu的id
     */
    public void createOrUpdateIndex(Long spuId) {
        // 查询spu
        Spu spu = goodsApi.querySpuById(spuId);
        // 构建goods
        Goods goods = this.buildGoods(spu);
        // 存入索引库（先根据id进行查询，如果没有则进行新增，如果已经存在则进行修改）
        goodsRepository.save(goods);
    }

    /**
     * description: 对索引库进行商品的删除
     * @author 朱正磊
     * @date 2021-04-14 01:25:28
     * @param spuId spu的id
     */
    public void deleteIndex(Long spuId) {
        // 从索引库删除指定商品
        goodsRepository.deleteById(spuId);
    }

}
